#-------------------------------------------------------------------------------
# Formats for some parameters of ARDataContainer.
#-------------------------------------------------------------------------------

# Storage for each resource relevant data.
# ie, a resource name "zone" may have a plural "zones", a model Zone and a name_prefix
# depending of it's parent: ie "country_". 
# This will be inferred from the info on routes.rb
Struct.new "ARResource",    :singular, :plural, :model, :model_name, :name_prefix

# This will hold and array of field, title_of_field to display on each view.
Struct.new "ARFields",      :for_index, :for_new, :for_edit, :for_show

# Configuration of the breadcrumbs
Struct.new "ARBreadcrumbs", :prefix_controller, :prefix_label, 
                            :plural, :singular, :label_new, :separator

# will hold the templates to render for each of the views and partials needed
Struct.new "ARTemplates",   :index, :show, :edit, :new, :search, 
                            :index_partial, :search_results

# Used to enable / disable additional options and to pass parameters
# For now, i use it to configure ferret and will paginate support
Struct.new "ARAddon",       :enabled, :options

# This is used on a fields definition to signal that the value  is the result of
# calling a helper method.
Struct.new "ARHelperCall",  :method_name

module AutoRest
  
  # List of columns that will be removed from views by default 
  # when no show, edit or index columns are specified. 
  # Also, any field finished in _count will be removed by default.
  AR_MAGIC_COLUMNS= %w|
    created_at created_on updated_at updated_on lock_version type     id
    position   parent_id  lft        rgt        quote        template
  |
  
  module Configuration
    
    #---------------------------------------------------------------------------
    # Main AutoRest options container. One ARDataContainer object should be 
    # available for each ActionController derived controller managed by AutoRest. 
    # AutoRest data will by available by means of an ardata class method
    #---------------------------------------------------------------------------
    class ARDataContainer
      
      # Each option's name and type. I keep also an array to store the order
      # of validation, wich can be important (one option depending of another)
      @@option_names= [] # An array to store the order of the options
      @@option_types= {} # A hash to associate a type.
      cattr_accessor :option_names
      
      class << self
        # Make accesors for options with special attr. reader.
        # These accesors yields the option if a block is given, 
        # and uses lazy initialization with a given class
        def add_ar_option name, klass= OpenStruct
          @@option_names << name
          @@option_types[name]= klass

          attr_writer name
          
          class_eval(<<-END_OF_READER)
            def #{name}
              @#{name}= #{klass.inspect}.new if NilClass === @#{name}
              yield(@#{name}) if block_given?
              @#{name}
            end
          END_OF_READER
        end
      end
  
      # Call check methods for all options. This means that, for each option,
      # "check_#{option_name}" method should be defined.
      # Also check that the class of the option is the wanted. Altough this may not
      # sound as the "ruby-way", In this case is very helpfull as there are 
      # a lot of options to configure and a lot of mistakes to make!
      # Needed parameters are: resource, resources_parents, controller
      def check_all_options options= {}
        @res= options[:resource]
        @res_parents= options[:resources_parents]
        @controller= options[:controller]
        
        @@option_names.each do |opt_name| 
          if not @@option_types[opt_name] === self.send(opt_name)
            message= "You shold not change type for option #{opt_name}.\n" \
                     "Type should be #{@@option_types[opt_name]}."
            throw StandardError.new(message)
          end
          self.send("check_#{opt_name}")
        end
      end
      
    private
    
    # --------------------------------------------------------------------------
    # Here I define the needed options.
    # --------------------------------------------------------------------------
      
      add_ar_option :resource,          Struct::ARResource
      add_ar_option :parent_resources,  Array
      add_ar_option :parent_ids,        Array
      add_ar_option :foreing_models,    Hash
      add_ar_option :index_finder,      Hash
      
      add_ar_option :use_ajax,          Object
      
      add_ar_option :exclude_fields,    Struct::ARFields
      add_ar_option :fields,            Struct::ARFields
      add_ar_option :hidden_fields,     Array
      
      add_ar_option :breadcrumbs,       Struct::ARBreadcrumbs
      add_ar_option :templates,         Struct::ARTemplates
      add_ar_option :labels,            Hash
      
      add_ar_option :exclude_methods,   Array
      add_ar_option :only_methods,      Array
      add_ar_option :methods,           Array
      
      add_ar_option :exclude_links,     Array
      add_ar_option :only_links,        Array
      add_ar_option :links,             Array
      
      add_ar_option :ferret,            Struct::ARAddon
      add_ar_option :will_paginate,     Struct::ARAddon
      
      # --------------------------------------------------------------------------
      # I Check the options and provide defaults if the user has not defined the option
      # --------------------------------------------------------------------------
      def check_resource
        resource.singular    ||= @res.singular.to_s
        resource.plural      ||= @res.plural.to_s
        resource.model       ||= @res.singular.camelize.constantize
        resource.name_prefix ||= @res.name_prefix || ''
        resource.model_name    = resource.model.class_name.to_s # Always should be te same as resource.model.class_name
      rescue => e
        raise StandardError.new("** AutoRest: model not found supplying defaults for #{@res.singular.camelize}.\n#{e.message}")
      end
      
      # --------------------------------------------------------------------------
      # Check for defaults to parent_resources
      # The resource may have many parents, so i try to verify this.
      # --------------------------------------------------------------------------    
      def check_parent_resources
        # Key to hash for controller relationships information.
        ar_parents_key= @controller.controller_class_name.intern
        parents= @res_parents[@controller.controller_name]

        if !parents || parents.empty?
          @controller.ar_controller_parents[ar_parents_key]= Set.new
        else
          parents.each do |controller_name|
            AutoRest::RailsResources.of_controller(controller_name).each do |parent_res|
              
              # I don't support nesting more than one level yet. Sorry!
              next if parent_res.name_prefix && !parent_res.name_prefix.empty?
              
              self.parent_resources << Struct::ARResource.new(parent_res.singular.to_s, 
                                                              parent_res.plural.to_s,
                                                               parent_res.singular.camelize.constantize,
                                                               parent_res.singular.capitalize,
                                                               ("#{parent_res.singular.to_s}_" || ''))
                                                       
              parent_controller= "#{parent_res.plural.to_s.camelize}Controller".intern
              
              (@controller.ar_controller_parents[ar_parents_key]||= Set.new) << parent_controller
            end
          end
        end
        self.parent_resources= Set.new(self.parent_resources)
      end
      
      # --------------------------------------------------------------------------
      # Get all parent ids from parent resources and from model resource AR reflections.
      # --------------------------------------------------------------------------    
      def check_parent_ids
        # This should not be necesary as parent_ids should be included on reflections.
        #parent_resources.each { |res| parent_ids << "#{res.singular}_id" }

        # Model foreign keys 
        resource.model.reflections.each do |key, refl|
          if refl.macro == :belongs_to || refl.macro == :has_and_belongs_to_many
            parent_ids << refl.primary_key_name
            foreing_models[refl.primary_key_name.to_s]= refl.class_name.constantize
          end
        end
        self.parent_ids= Set.new(self.parent_ids)
      end
      
      # --------------------------------------------------------------------------
      # Try to get models from parent ids.
      # --------------------------------------------------------------------------        
      def check_foreing_models
        parent_ids.each do |attr|
          next if attr !~ /(.*)_id$/
          k= $1.camelize
          begin;  foreing_models[attr]= k.singularize.constantize
          rescue
          end
        end
      end
      
      # --------------------------------------------------------------------------
      # Options to feed the resource finder 
      # (i.e. ARModel.find :all, :options => index_finder, etc...)
      # --------------------------------------------------------------------------          
      def check_index_finder
        # Leave it alone
      end      
      
      
      # --------------------------------------------------------------------------
      # Check fields to be removed from columns.
      # --------------------------------------------------------------------------          
      def check_exclude_fields
=begin
        [:for_index, :for_new, :for_edit, :for_show].each do |key|
          exc= exclude_fields.send(key)
          next if exc # Leave user options if given.
          
          # By default, the foreign key of the parents are assigned on nested resources,
          # so i remove the from the default columns of new
          exclude_fields.send("#{key}=", parent_ids) if key == :for_new && !parent_ids.empty?
        end
=end
        # Because a single resource can have multiple resource parents,
        # this check is going to be done on "runtime" (ie remove the field for the 
        # current parent, not all parents' fields)
      end
      
      # Normalize the choice for ajax. The option is initialized as Object.new. 
      # The default is to use Ajax (true). Every ajax feature will check for this 
      # before doing its job.
      def check_use_ajax
        case self.use_ajax
        when Object, TrueClass
          self.use_ajax= true
        else
          self.use_ajax= false
        end
      end
      
      # --------------------------------------------------------------------------
      # Check columns (model attributes) available in each view.
      # --------------------------------------------------------------------------    
      def check_fields
        columns= resource.model.column_names - AutoRest::AR_MAGIC_COLUMNS

        default_columns= columns.select { |attr| attr !~ /.*_count$/ }.map do |elem|
          [elem.intern, elem.titleize]
        end

        # For storing columns, I use an array and not a hash --i.e.: (:column => 'Title') -- 
        # to ensure the order of the columns. I convert the params from linear array to 
        # grouped array to alivianate user's data entry:
        # [[:col0, "name0"], [:col1, "name1"]] becomes [:col0, "name0", :col1, "name1"]
        
        # *TODO* This should be cleaned a little, because right now is a bit difficult
        #        to remember the way the attributes for each view work.

        [:for_index, :for_new, :for_edit, :for_show].each do |key|
          arr= fields.send(key)
          exc= exclude_fields.send(key)

          if !arr # User has not supplied columns.
            arr= default_columns
          else
            # enum_for creates an enum from array, therefore protecting the original array contents.
            # each slice returns an array of specified elements: [1,2,3,4] => 1..4 => [[1,2],[3,4]]
            arr= arr.enum_for(:each_slice, 2).to_a
          end
          
          # Remove excluded fields.
          arr= arr.reject { |e| exc.map{|elem|elem.to_s}.include?(e[0].to_s) } if exc
          fields.send("#{key}=", arr)
        end
      end

      # --------------------------------------------------------------------------
      # Hidden columns for storing params on view's form.
      # --------------------------------------------------------------------------    
      def check_hidden_fields
        self.hidden_fields= Set.new(self.hidden_fields)
      end
      
      # --------------------------------------------------------------------------
      # Breadcrumbs configuration.
      # --------------------------------------------------------------------------    
      def check_breadcrumbs
      # breadcrumbs.prefix_controller # Leave it alone 
      # breadcrumbs.prefix_label      # Leave it alone 
        breadcrumbs.plural    ||= "#{resource.plural.titleize}&nbsp;"
        breadcrumbs.singular  ||= "#{resource.singular.titleize}&nbsp;"
        breadcrumbs.label_new ||= AutoRest::Labels.defaults[:breadcrumbs_newlabel]
        breadcrumbs.separator ||= AutoRest::Labels.defaults[:breadcrumbs_separator]
      end
      
      # --------------------------------------------------------------------------
      # Templates for Views 
      # --------------------------------------------------------------------------    
      def check_templates

        %w|index show edit new search|.each do |template|
          if not templates.send(template)
            templates.send("#{template}=", { :template => "auto_rest_shared/#{template}" })
          end
        end
        
        self.templates.index_partial||= { :partial => "auto_rest_shared/index_items", :layout => false }
        self.templates.search_results||= { :partial => "auto_rest_shared/search_results", :layout => false }
      end
      
      # --------------------------------------------------------------------------
      # Labels for Views 
      # --------------------------------------------------------------------------    
      def check_labels
        if !self.labels[:search_title]
          search_title= AutoRest::Labels.defaults[:search_title] + " " + resource.plural.titleize
          self.labels[:search_title]= "<h2>#{search_title}</h2>"
        end
        
        self.labels.reverse_merge! AutoRest::Labels.defaults
      end

      # --------------------------------------------------------------------------
      # Methods to generate or include.
      # --------------------------------------------------------------------------    
      def check_only_methods
      end
      
      def check_exclude_methods 
      end
      
      def check_methods
        all_methods= [:index, :show, :edit, :create, :update, :new, :destroy, :search]

        # .map{ |e| e.to_s.intern } => Allow strings or symbols.
        self.methods= Set.new(case
        when self.only_methods && !self.only_methods.empty?
          self.only_methods.map{ |e| e.to_s.intern }
        when self.exclude_methods && !self.exclude_methods.empty?
          all_methods - self.exclude_methods.map{ |e| e.to_s.intern }
        else
          all_methods
        end)
      end

      # --------------------------------------------------------------------------
      # Links to include on views.
      # --------------------------------------------------------------------------          
      def check_only_links
      end
      
      def check_exclude_links
      end
      
      def check_links
        all_links= [:index, :show, :edit, :create, :update, :new, :destroy, :back, :search]

        # I almost feel bad duplicating the previous "check_methods" method.... NAAAHHH! :)
        self.links= Set.new(case
        when self.only_links && !self.only_links.empty?
          self.only_links.map{ |e| e.to_s.intern }
        when self.exclude_links && !self.exclude_links.empty?
          all_links - self.exclude_links.map{ |e| e.to_s.intern }
        else
          all_links
        end)
      end

      # --------------------------------------------------------------------------
      # Options for integration with ferret search engine.
      # --------------------------------------------------------------------------
      def check_ferret
        if NilClass === self.ferret.enabled && (defined?(::Ferret) != nil) 
          # Look for a search action relative to a collection of resources.
          search_route_present= @res.collection_methods.values.flatten.include? :search # << Resource class of Rails
          self.ferret.enabled= search_route_present && resource.model.respond_to?(:find_by_contents)
        end
        
        if self.ferret.enabled
          defaults= { 
            :search_limit => :all 
          }
          (self.ferret.options||={}).reverse_merge!(defaults)
        else
          self.links.delete :search
          self.methods.delete :search
        end
      end
      
      # --------------------------------------------------------------------------
      # Options for integration with will_paginate plugin.
      # --------------------------------------------------------------------------
      def check_will_paginate
        if NilClass === self.will_paginate.enabled
          self.will_paginate.enabled= (defined?(::WillPaginate) != nil)
        end
        
        if self.will_paginate.enabled
          defaults = {
            :prev_label => AutoRest::Labels.defaults[:pagination_prev], 
            :next_label => AutoRest::Labels.defaults[:pagination_next],
            :inner_window => 2, # how many links are shown around the current page, defaults to 4
            :outer_window => 1, # how many links are around the first and the last page, defaults to 1
            :param_name   => :page
          }
          ajax_defaults= {
            :remote_options => {
              :method => :get,
              :update => "auto_rest_index_table",
              :before => "Element.show('spinner')",
              :success => "Element.hide('spinner')" 
            }           
          }
          (self.will_paginate.options||={}).reverse_merge!(defaults)
           self.will_paginate.options.reverse_merge!(ajax_defaults) if self.use_ajax
        end
      end
    end
    
  end
end